
// Higher-level API for dealing with the social graph.

// The basic approach here is to fetch as much data as possible
// while answering a single question and cache the results.

var SocialGraph = {};

(function () {

    var nodeCache = {};
    var metaNodes = {};

    var getUsernameForURL = function (url) {
        if (url.match(/^sgn:/)) {
            var result = url.match(/\?ident=(.*)$/);
            if (result) {
                return result[1];
            }
            else {
                return undefined;
            }
        }
        else {
            return undefined;
        }
    };

    var getPKForURL = function (url) {
        if (url.match(/^sgn:/)) {
            var result = url.match(/\?pk=(.*)$/);
            if (result) {
                return result[1];
            }
            else {
                return undefined;
            }
        }
        else {
            return undefined;
        }
    };

    var getFullNameForURL = function (url) {
        // HACK: Facebook URLs have the user's name embedded in them.
        // This'll work as long as someone doesn't make a rel="me"
        // with the wrong name, since Facebook doesn't actually check
        // the name when resolving the URL.
        if (url.match(/^sgn:\/\/facebook.com\//)) {
            var username = getUsernameForURL(url);
            if (username) {
                var result = username.match(/^([\w\.,]+)\/(\d+)$/);
                if (result) {
                    return result[1].replace('_', ' ');
                }
                else {
                    return undefined;
                }
            }
            else {
                return undefined;
            }
        }
        else {
            var node = nodeCache[url];
            if (node) {
                var fn = node.getAttributeByName("fn");
                return fn ? fn : undefined;
            }
            else {
                return undefined;
            }
        }
    };

    var getDomainForURL = function (url) {
        if (url.match(/^sgn:/)) {
            var result = url.match(/^sgn:\/\/([^\/]+)\//);
            if (result) {
                return result[1];
            }
            else {
                return undefined;
            }
        }
        else {
            return undefined;
        }
    };

    var getAttributeForURL = function (attributeName, url) {
        var node = nodeCache[url];
        if (node) {
            return node.getAttributeByName(attributeName);
        }
        else {
            return undefined;
        }
    };

    var getMostPopularResponses = function (list, func) {
        var responseCount = {};
        var responses = [];

        for (var i = 0; i < list.length; i++) {
            var item = list[i];
            var response = func(item);
            if (response) {
                if (! responseCount[response]) {
                    responses.push(response);
                    responseCount[response] = 0;
                }
                responseCount[response]++;
            }
        }

        return responses.sort(function (a, b) {
            var ac = responseCount[a];
            var bc = responseCount[b];
            var diff = bc - ac;
            if (diff != 0) {
                return diff;
            }
            else {
                // Prefer shorter responses
                // (this is really only useful for usernames)
                return a.length - b.length;
            }
        });
    };

    var Node = function (ident, data) {
        this.ident = ident;
        this.data = data;
        this.metaNode = undefined;
    };
    Node.prototype = {};
    Node.prototype.getIdentifier = function () {
        return this.ident;
    };
    Node.prototype.getUsername = function () {
        return getUsernameForURL(this.ident);
    };
    Node.prototype.getFullName = function () {
        return getFullNameForURL(this.ident);
    };
    Node.prototype.getSGNDomain = function () {
        return getDomainForURL(this.ident);
    };
    Node.prototype.getSGNPK = function () {
        return getPKForURL(this.ident);
    };
    Node.prototype.getClaimedNodeIdentifiers = function () {
        return this.data.claimed_nodes.slice();
    };
    Node.prototype.getAttributeByName = function (name) {
        return this.data.attributes[name];
    };
    Node.prototype.fetchClaimedNodes = function (callback) {
        var claimedNodes = this.getClaimedNodeIdentifiers();
        SocialGraph.fetchSeveralNodes(claimedNodes, function (data) {
            var ret = [];
            for (var url in data) {
                ret.push(data[url]);
            }
            callback(ret);
        });
    };
    Node.prototype.getMetaNode = function () {
        return this.metaNode;
    };
    Node.prototype.getReferencedNodeURLs = function () {
        var referenceData = this.data.nodes_referenced;

        var ret = [];
        for (var url in referenceData) {
            ret.push([url, referenceData[url].types]);
        }
    };
    Node.prototype.fetchReferencedNodes = function (callback) {
        var referenceData = this.data.nodes_referenced;

        var urls = [];
        for (var url in referenceData) {
            urls.push(url);
        }
        SocialGraph.fetchSeveralNodes(urls, function (result) {
            var ret = [];
            for (var url in referenceData) {
                ret.push([ result[url], referenceData[url].types ]);
            }
            callback(ret);
        });
    };
    Node.prototype.getConnectionsToNode = function (node) {
        var otherURL = node.getIdentifier();

        if (this.data.nodes_referenced[otherURL]) {
            return this.data.nodes_referenced[otherURL].types;
        }
        else {
            return [];
        }
    };

    var metaNodeIndex = 0;
    // A "MetaNode" is a collection of Nodes that all refer to the same person
    var MetaNode = function () {
        this.members = [];
        this.index = metaNodeIndex++;
        metaNodes[this.index] = this;
    };
    MetaNode.prototype = {};
    MetaNode.prototype.getIndex = function () {
        return this.index;
    };
    MetaNode.prototype.getNodeURLs = function () {
        return this.members.slice();
    };
    MetaNode.prototype.getNodes = function () {
        // All nodes that are in a MetaNode must be in the cache,
        // so we don't need to actually do any async network stuff
        // here.
        var ret = [];
        for (var i = 0; i < this.members.length; i++) {
            var url = this.members[i];
            if (nodeCache[url]) {
                ret.push(nodeCache[url]);
            }
        }
        return ret;
    };
    MetaNode.prototype.fetchNodes = function (callback) {
        // We ought to have all of the members loaded already, but since
        // we've got the opportunity to be async here let's fill in any
        // cache holes we've ended up with.

        var urlsToFetch = [];
        var ret = [];

        for (var i = 0; i < this.members.length; i++) {
            var url = this.members[i];
            if (nodeCache[url]) {
                ret.push(nodeCache[url]);
            }
            else {
                urlsToFetch.push(url);
            }
        }

        if (urlsToFetch.length == 0) {
            // We have everything, so return immediately.
            callback(ret);
        }
        else {
            // We do have some cache holes, so let's fill them in.
            SocialGraph.fetchSeveralNodes(urlsToFetch, function (nodes) {
                for (var i = 0; i < nodes.length; i++) {
                    ret.push(nodes[i]);
                }
                callback(ret);
            });
        }
    };
    MetaNode.prototype.addNode = function (node) {
        var oldMetaNode = node.metaNode;
        if (oldMetaNode) {
            if (oldMetaNode == this) {
                // Already a member. Nothing to do.
            }
            else {
                // Need to merge our two metanodes.
                this.mergeAndDestroy(oldMetaNode);
            }
        }
        else {
            this.members.push(node.getIdentifier());
            node.metaNode = this;
        }
    };
    MetaNode.prototype.mergeAndDestroy = function (otherMetaNode) {
        // Once this function returns, otherMetaNode will no
        // longer have any members and all of its members will be
        // in thisMetaNode.

        var thisMetaNode = this;

        // Sanity check
        if (thisMetaNode == otherMetaNode) return;

        var newNodes = otherMetaNode.getNodes();

        for (var i = 0; i <= newNodes.length; i++) {
            var node = newNodes[i];
            if (node) {
                node.metaNode = this;
            }
            else {
                console.log("Encountered null node?!");
                continue;
            }
            this.members.push(node.getIdentifier());
        }

        // Kill the old node and turn the old id into an alias
        // for this one.
        otherMetaNode.members = [];
        metaNodes[otherMetaNode.id] = this;
    };
    MetaNode.prototype.isDead = function () {
        return this.members.length > 0 ? false : true;
    }
    MetaNode.prototype.getUsernames = function () {
        // Get a list of usernames used by the person who this
        // MetaNode represents, ordered by the number of sites
        // the username is used on.

        return getMostPopularResponses(this.members, getUsernameForURL);
    };
    MetaNode.prototype.getFullNames = function () {
        return getMostPopularResponses(this.members, getFullNameForURL);
    };
    MetaNode.prototype.getValuesForAttribute = function (attributeName) {
        return getMostPopularResponses(this.members, function (url) {
            return getAttributeForURL(attributeName, url);
        });
    };
    MetaNode.prototype.getReferencesToNode = function (node) {
        var memberNodes = this.getNodes();
        var ret = [];

        for (var i = 0; i < memberNodes.length; i++) {
            var myNode = memberNodes[i];
            var connections = myNode.getConnectionsToNode(node);
            if (connections.length) {
                ret.push([connections, myNode]);
            }
        }

        return ret;
    };
    MetaNode.prototype.fetchReferencedMetaNodes = function (callback) {
        var nextItem = 0;
        var nodes = this.getNodes();
        var numItems = nodes.length;

        var nodeReferences = [];

        var fetchAnother;
        fetchAnother = function () {
            var node = nodes[nextItem++];
            node.fetchReferencedNodes(function (references) {
                for (var i = 0; i < references.length; i++) {
                    nodeReferences.push(references[i]);
                }

                if (nextItem < numItems) {
                    // Still more to do
                    fetchAnother();
                }
                else {
                    // Now we turn the list of nodes into a list of metanodes,
                    // via a hashtable to avoid duplicates.

                    var metaNodeInfo = {};

                    for (var i = 0; i < nodeReferences.length; i++) {
                        var node = nodeReferences[i][0];
                        var edgeTypes = nodeReferences[i][1];

                        if (! node) {
                            // FIXME: There seems to be a case where we get back
                            // undefined nodes, but I've not spotted why that is yet.
                            // For now, let's just ignore them.
                            console.log("Encountered undefined node");
                            continue;
                        }

                        var metaNode = node.getMetaNode();
                        var metaNodeIdx = metaNode.index;
                        if (! metaNodeInfo[metaNodeIdx]) {
                            metaNodeInfo[metaNodeIdx] = {
                                metaNode: metaNode,
                                edgeTypes: {}
                            };
                        }

                        var thisInfo = metaNodeInfo[metaNodeIdx];

                        for (var j = 0; j < edgeTypes.length; j++) {
                            if (edgeTypes[j] != "me") {
                                thisInfo.edgeTypes[edgeTypes[j]] = true;
                            }
                        }

                    }

                    // Now project the metaNodeInfo structure into the flat list
                    // we'll return.
                    var ret = [];

                    for (metaNodeIdx in metaNodeInfo) {
                        var thisInfo = metaNodeInfo[metaNodeIdx];
                        var edgeTypes = [];
                        for (edgeType in thisInfo.edgeTypes) {
                            edgeTypes.push(edgeType);
                        }
                        ret.push([thisInfo.metaNode, edgeTypes]);
                    }

                    callback(ret);
                }
            });
        };

        fetchAnother();

    };

    var fetchAndCache = function (urls, callback) {
        var args = {};
        args.sgn = true;
        args.fme = true;
        args.edo = true;
        args.edi = true;

        // Remove all from the request array all of the URLs
        // we already have in cache, since there's no point
        // in requesting them again.
        var removedUrls = [];
        var urlsToRequest = [];
        for (var i = 0; i < urls.length; i++) {
            var url = urls[i];
            if (nodeCache[url]) {
                removedUrls.push(url);
            }
            else {
                urlsToRequest.push(url);
            }
        }

        var realCallback;
        realCallback = function (result) {
            if (result.nodes) {
                // Copy all of the returned nodes into our cache
                for (var url in result.nodes) {
                    var data = result.nodes[url];
                    nodeCache[url] = new Node(url, data);
                }
            }

            var urlsMissingFromCache = [];

            if (result.canonical_mapping) {
                // Create aliases in the cache for any non-canonical URLs
                for (var url in result.canonical_mapping) {
                    var canonURL = result.canonical_mapping[url];
                    if (canonURL && nodeCache[canonURL] && ! nodeCache[url]) {
                        nodeCache[url] = nodeCache[canonURL];
                    }
                }

                // Create or update metanodes for the returned nodes.
                for (var url in result.canonical_mapping) {
                    var canonURL = result.canonical_mapping[url];
                    var node = nodeCache[canonURL];
                    var metaNode = new MetaNode();
                    metaNode.addNode(node);
                    var claimedIdentifiers = node.getClaimedNodeIdentifiers();
                    for (var i = 0; i < claimedIdentifiers.length; i++) {
                        var claimedURL = claimedIdentifiers[i];
                        var claimedNode = nodeCache[claimedURL];
                        // It ought to be in the cache, but if not we make a note of it
                        // as something to go and fetch before we return.
                        if (claimedNode) {
                            metaNode.addNode(claimedNode);
                        }
                        else {
                            console.log(claimedURL, " is not in the nodecache. Huh?");
                            urlsMissingFromCache.push(claimedURL);
                        }
                    }
                }
            }

            if (urlsMissingFromCache.length > 0) {
                // We had some stuff missing, so let's run off and fetch
                // the missing bits and then try this callback again.
                console.log("Attempting to fill some holes in the cache...");
                SocialGraph.fetchSeveralNodes(urlsMissingFromCache, function () {
                    console.log("Cache catchup completed. Let's try that callback again.");
                    realCallback(result);
                });
                return;
            }

            // Now we need to fill in all of the data for the bits
            // we removed earlier so that the caller gets what it's
            // expecting.
            for (var i = 0; i < removedUrls.length; i++) {
                var url = removedUrls[i];
                result.canonical_mapping[url] = url;
                result.nodes[url] = nodeCache[url].data;
            }

            callback(result);
        };

        if (urlsToRequest.length) {
            SGAPI.fetchResults(urlsToRequest, args, realCallback);
        }
        else {
            // Everything the caller asked for is in the cache,
            // so let's just return immediately.
            realCallback({
                canonical_mapping: {},
                nodes: {}
            });
        }

    };

    SocialGraph.fetchNode = function (url, callback) {
        if (nodeCache[url]) {
            callback(nodeCache[url]);
        }
        else {
            fetchAndCache([ url ], function (data) {
                var canonUrl = data.canonical_mapping[url];
                // By now we ought to have the node in our cache
                callback(nodeCache[canonUrl]);
            });
        }
    };

    SocialGraph.fetchSeveralNodes = function (urls, callback) {
        fetchAndCache(urls, function(data) {
            var ret = {};
            for (var i = 0; i < urls.length; i++) {
                var url = urls[i];
                var canonUrl = data.canonical_mapping[url];
                ret[url] = nodeCache[canonUrl];
            }
            callback(ret);
        });
    };

    SocialGraph.seedCacheWithURLs = function (urls, callback) {
        var innerCallback = function (result) {
            callback();
        };
        fetchAndCache(urls, callback);
    }

    SocialGraph.getMetaNode = function (index) {
        return metaNodes[index];
    };

})();

